package tools.redistools;


import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.intellij.lang.annotations.Language;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.SpelCompilerMode;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import static tools.CompareUtils.greaterThanZero;


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface AroundDisLock {

    String code();

    String delimiter() default ":";

    @Language(value = "SpEL") String key();

    @Language(value = "SpEL") String expand() default "";

    long ttl() default 30L;
}

@Aspect
@Component
@Slf4j
class AroundDisLockImpl implements BeanFactoryAware {

    private final RedisDisLock redisDisLock;

    private BeanFactory beanFactory;

    private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

    /**
     * 定义spel表达式解析器
     */
    private static final SpelExpressionParser spelExpressionParser = new SpelExpressionParser(
        new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, Thread.currentThread().getContextClassLoader())
    );

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    /**
     * 定义context的参数：切面中获取method、parameters...
     *
     * @param joinPoint
     * @return
     */
    private MethodBasedEvaluationContext getContext(ProceedingJoinPoint joinPoint) {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        MethodBasedEvaluationContext context = new MethodBasedEvaluationContext(
            joinPoint.getTarget(),
            method,
            joinPoint.getArgs(),
            parameterNameDiscoverer
        );
        context.setBeanResolver(new BeanFactoryResolver(this.beanFactory));
        return context;
    }

    /**
     * 解析spel表达式
     *
     * @param key
     * @return
     */
    @SneakyThrows
    private Expression getExpression(String key) {
        return spelExpressionParser.parseExpression(key);
    }

    @Autowired
    public AroundDisLockImpl(RedisDisLock redisDisLock) {
        this.redisDisLock = redisDisLock;
    }

    @Pointcut("@annotation(com.dream.yuxiaor.service.contract.annotation.AroundDisLock)")
    void handle() {
    }

    @SneakyThrows
    @Around("handle() && @annotation(ad)")
    public Object invoke(ProceedingJoinPoint joinPoint, AroundDisLock ad) {
        MethodBasedEvaluationContext context = this.getContext(joinPoint);
        String code = ad.code();
        String delimiter = ad.delimiter();
        long ttl = ad.ttl();
        Object key = getExpression(ad.key()).getValue(context);
        Long companyId = (Long) getExpression(ad.expand()).getValue(context);
        List<Long> paramList = new ArrayList<>();
        if (key instanceof ContractInfoModel) {
            ContractInfoModel model = (ContractInfoModel) key;
            if (greaterThanZero(companyId)) {
                paramList.add(greaterThanZero(companyId) ? companyId : 0L);
                paramList.add(greaterThanZero(model.getHouseId()) ? model.getHouseId() : 0L);
                paramList.add(greaterThanZero(model.getRoomId()) ? model.getRoomId() : 0L);
                log.info(code + " add lock...");
                redisDisLock.redisDistributeLock(code, ttl, delimiter, paramList);
                Object proceed = joinPoint.proceed();
                log.info(code + " release lock...");
                redisDisLock.delRedisDistributeLock(code, delimiter, paramList);
                return proceed;
            }
        }
        return null;
    }

    /**
     * 异常解锁
     *
     * @param ad
     * @param e
     */
    @AfterThrowing(value = "handle() && @annotation(ad)", throwing = "e")
    public void afterException(JoinPoint joinPoint, AroundDisLock ad, Exception e) {
        if (e instanceof RuntimeException) {
                MethodBasedEvaluationContext context = this.getContext((ProceedingJoinPoint) joinPoint);
                Object key = getExpression(ad.key()).getValue(context);
                Long companyId = (Long) getExpression(ad.expand()).getValue(context);
                List<Long> paramList = new ArrayList<>();
                if (key instanceof ContractInfoModel) {
                    ContractInfoModel model = (ContractInfoModel) key;
                    if (greaterThanZero(companyId)) {
                        log.error(ad.code() + " caught error，e : " + e);
                        paramList.add(greaterThanZero(companyId) ? companyId : 0L);
                        paramList.add(greaterThanZero(model.getHouseId()) ? model.getHouseId() : 0L);
                        paramList.add(greaterThanZero(model.getRoomId()) ? model.getRoomId() : 0L);
                        log.info(ad.code() + " release lock...");
                        redisDisLock.delRedisDistributeLock(ad.code(), ad.delimiter(), paramList);
                    }
                }
            }
        }
}
